<?php
namespace Core;

use Exception;
use ReflectionClass;

class Container {
    protected $bindings = [];
    protected $instances = [];
    
    public function bind($abstract, $concrete = null, $shared = false) {
        if (is_null($concrete)) {
            $concrete = $abstract;
        }
        
        $this->bindings[$abstract] = compact('concrete', 'shared');
    }
    
    public function singleton($abstract, $concrete = null) {
        $this->bind($abstract, $concrete, true);
    }
    
    public function make($abstract, array $parameters = []) {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }
        
        if (!isset($this->bindings[$abstract])) {
            return $this->build($abstract, $parameters);
        }
        
        $concrete = $this->bindings[$abstract]['concrete'];
        
        if ($concrete instanceof \Closure) {
            $object = $concrete($this, $parameters);
        } else {
            $object = $this->build($concrete, $parameters);
        }
        
        if (isset($this->bindings[$abstract]['shared']) && $this->bindings[$abstract]['shared']) {
            $this->instances[$abstract] = $object;
        }
        
        return $object;
    }
    
    public function build($concrete, array $parameters = []) {
        try {
            $reflector = new ReflectionClass($concrete);
        } catch (\ReflectionException $e) {
            throw new Exception("Target class [$concrete] does not exist.");
        }
        
        if (!$reflector->isInstantiable()) {
            throw new Exception("Target class [$concrete] is not instantiable.");
        }
        
        $constructor = $reflector->getConstructor();
        
        if (is_null($constructor)) {
            return new $concrete;
        }
        
        $dependencies = $constructor->getParameters();
        $instances = $this->resolveDependencies($dependencies, $parameters);
        
        return $reflector->newInstanceArgs($instances);
    }
    
    protected function resolveDependencies(array $dependencies, array $parameters) {
        $results = [];
        
        foreach ($dependencies as $dependency) {
            if (array_key_exists($dependency->name, $parameters)) {
                $results[] = $parameters[$dependency->name];
                continue;
            }
            
            if ($dependency->isDefaultValueAvailable()) {
                $results[] = $dependency->getDefaultValue();
                continue;
            }
            
            if (!is_null($dependency->getType()) && !$dependency->getType()->isBuiltin()) {
                $results[] = $this->make($dependency->getType()->getName());
                continue;
            }
            
            throw new Exception("Unable to resolve dependency [{$dependency->name}]");
        }
        
        return $results;
    }
}